// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Runs the ffigen configs, then merges tool/data/extra_methods.dart.in into the
// Objective C bindings.

// ignore_for_file: avoid_print

import 'dart:io';

import 'package:args/args.dart';
import 'package:ffigen/src/executables/ffigen.dart' as ffigen;
import 'package:yaml/yaml.dart';

const cConfig = 'ffigen_c.yaml';
const objcConfig = 'ffigen_objc.yaml';
const cBindings = 'lib/src/c_bindings_generated.dart';
const objcBindings = 'lib/src/objective_c_bindings_generated.dart';
const objcExports = 'lib/src/objective_c_bindings_exported.dart';
const extraMethodsFile = 'tool/data/extra_methods.dart.in';
const builtInTypes =
    '../ffigen/lib/src/code_generator/objc_built_in_types.dart';
const interfaceListTest = 'test/interface_lists_test.dart';

const privateInterfaces = <String>{
  'DartInputStreamAdapter',
  'DartInputStreamAdapterWeakHolder',
  'DOBJCObservation',
};

final privateMethods = <String>{
  for (final name in privateInterfaces) '$name\$Methods',
};
final privateClasses = privateInterfaces.union(privateMethods);

void dartCmd(List<String> args) {
  final exec = Platform.resolvedExecutable;
  final proc = Process.runSync(exec, args, runInShell: true);
  if (proc.exitCode != 0) {
    exitCode = proc.exitCode;
    print(proc.stdout);
    print(proc.stderr);
    throw Exception('Command failed: $exec ${args.join(" ")}');
  }
}

typedef ClassInfo = ({
  String name,
  String? ext,
  List<String> mix,
  List<String> impl,
});
final _clsDecl = RegExp(
  r'^class (.*?)(?: extends (.*?))?(?: with (.*?))?(?: implements (.*?))? {',
);
ClassInfo? parseClassDecl(String line) {
  final match = _clsDecl.firstMatch(line);
  if (match == null) return null;
  return (
    name: match[1]!,
    ext: match[2],
    mix: match[3]?.split(', ') ?? [],
    impl: match[4]?.split(', ') ?? [],
  );
}

typedef ExtraMethods = ({ClassInfo cls, String methods});
Map<String, ExtraMethods> parseExtraMethods(String filename) {
  final extraMethods = <String, ExtraMethods>{};
  ClassInfo? currentClass;
  late StringBuffer methods;
  for (final line in File(filename).readAsLinesSync()) {
    if (currentClass == null) {
      final cls = parseClassDecl(line);
      if (cls != null) {
        currentClass = cls;
        methods = StringBuffer();
      }
    } else {
      if (line == '}') {
        extraMethods[currentClass.name] = (
          cls: currentClass,
          methods: methods.toString(),
        );
        currentClass = null;
      } else {
        methods.writeln(line);
      }
    }
  }
  return extraMethods;
}

String classDecl(
  String name,
  String? ext,
  List<String> mix,
  List<String> impl,
) => [
  'class $name',
  if (ext != null) 'extends $ext',
  if (mix.isNotEmpty) 'with ${mix.join(', ')}',
  if (impl.isNotEmpty) 'implements ${impl.join(', ')}',
  '{',
].join(' ');

void mergeExtraMethods(
  String filename,
  Map<String, ExtraMethods> extraMethods,
) {
  final out = StringBuffer();
  for (final line in File(filename).readAsLinesSync()) {
    final cls = parseClassDecl(line);
    final extra = cls == null ? null : extraMethods[cls.name];
    if (cls == null || extra == null) {
      out.writeln(line);
    } else {
      out.writeln(
        classDecl(
          cls.name,
          extra.cls.ext ?? cls.ext,
          [...cls.mix, ...extra.cls.mix],
          [...cls.impl, ...extra.cls.impl],
        ),
      );
      out.writeln(extra.methods);
      extraMethods.remove(cls.name);
    }
  }

  // Matching classes have been removed from extraMethods. Write all the
  // remaining classes separately.
  for (final extra in extraMethods.values) {
    out.writeln('\n');
    out.writeln(
      classDecl(extra.cls.name, extra.cls.ext, extra.cls.mix, extra.cls.impl),
    );
    out.writeln(extra.methods);
    out.writeln('}');
  }

  File(filename).writeAsStringSync(out.toString());
}

List<String> writeBuiltInTypes(String config, String out) {
  final yaml = loadYaml(File(config).readAsStringSync()) as YamlMap;

  final s = StringBuffer();
  final exports = <String>{};

  s.write('''
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Generated by package:objective_c's tool/generate_code.dart.
''');

  Iterable<String> writeDecls(String name, String key) {
    final decls = yaml[key] as YamlMap;
    final renames = decls['rename'] as YamlMap? ?? YamlMap();
    final includes = decls['include'] as YamlList;

    final names = <String, String>{
      for (final inc in includes.map<String>((i) => i as String))
        inc: renames[inc] as String? ?? inc,
    };
    exports.addAll(names.values);
    final anyRenames = names.entries.any((kv) => kv.key != kv.value);
    final elements = anyRenames
        ? names.entries.map((kv) => "  '${kv.key}': '${kv.value}',")
        : names.keys.map((key) => "  '$key',");

    s.write('''

const $name = {
${elements.join('\n')}
};
''');
    return names.values;
  }

  final interfaces = writeDecls('objCBuiltInInterfaces', 'objc-interfaces');
  exports.addAll([for (final name in interfaces) '$name\$Methods']);
  writeDecls('objCBuiltInCompounds', 'structs');
  writeDecls('objCBuiltInEnums', 'enums');
  writeDecls('objCBuiltInProtocols', 'objc-protocols');
  writeDecls('objCBuiltInCategories', 'objc-categories');
  writeDecls('objCBuiltInGlobals', 'globals');

  File(out).writeAsStringSync(s.toString());

  return exports.difference(privateClasses).toList()..sort();
}

void writeExports(List<String> exports, String out) {
  File(out).writeAsStringSync('''
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Generated by package:objective_c's tool/generate_code.dart.

export 'objective_c_bindings_generated.dart'
    show
        ${exports.join(',\n        ')};
''');
}

Future<void> run({required bool format}) async {
  print('Generating C bindings...');
  await ffigen.main(['--no-format', '-v', 'severe', '--config', cConfig]);

  print('Generating ObjC bindings...');
  await ffigen.main(['--no-format', '-v', 'severe', '--config', objcConfig]);
  mergeExtraMethods(objcBindings, parseExtraMethods(extraMethodsFile));

  print('Generating objc_built_in_types.dart...');
  final exports = writeBuiltInTypes(objcConfig, builtInTypes);

  print('Generating objective_c_bindings_exported.dart...');
  writeExports(exports, objcExports);

  if (format) {
    print('Formatting bindings...');
    dartCmd(['format', cBindings, objcBindings, builtInTypes, objcExports]);
  }

  print('Running tests...');
  dartCmd(['test', interfaceListTest]);
}

Future<void> main(List<String> args) async {
  Directory.current = Platform.script.resolve('..').path;
  final argResults =
      (ArgParser()..addFlag(
            'format',
            help: 'Format the generated code.',
            defaultsTo: true,
            negatable: true,
          ))
          .parse(args);
  await run(format: argResults.flag('format'));
}
